package cn.newtest.common.dbutils;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import cn.newtest.common.dbutils.handlers.FieldMapper;

/**
 * 
 * @author WangChui(baiyibing@gmail.com)
 * @version 1.0 TODO
 * 
 */
public class BeanProcessor {
	private static final Log log = LogFactory.getLog(BeanProcessor.class);

	/**
	 * Special array value used by <code>mapColumnsToProperties</code> that
	 * indicates there is no bean property that matches a column from a
	 * <code>ResultSet</code>.
	 */
	protected static final int PROPERTY_NOT_FOUND = -1;

	/**
	 * Set a bean's primitive properties to these defaults when SQL NULL is
	 * returned. These are the same as the defaults that ResultSet get* methods
	 * return in the event of a NULL column.
	 */
	private static final Map primitiveDefaults = new HashMap();

	static {
		primitiveDefaults.put(Integer.TYPE, new Integer(0));
		primitiveDefaults.put(Short.TYPE, new Short((short) 0));
		primitiveDefaults.put(Byte.TYPE, new Byte((byte) 0));
		primitiveDefaults.put(Float.TYPE, new Float(0));
		primitiveDefaults.put(Double.TYPE, new Double(0));
		primitiveDefaults.put(Long.TYPE, new Long(0));
		primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
		primitiveDefaults.put(Character.TYPE, new Character('\u0000'));
	}

	/**
	 * Factory method that returns a new instance of the given Class. This is
	 * called at the start of the bean creation process and may be overridden to
	 * provide custom behavior like returning a cached bean instance.
	 * 
	 * @param c
	 *            The Class to create an object from.
	 * @return A newly created object of the Class.
	 * @throws SQLException
	 *             if creation failed.
	 */
	protected Object newInstance(Class c) throws SQLException {
		try {
			return c.newInstance();

		} catch (InstantiationException e) {
			throw new SQLException("Cannot create " + c.getName() + ": "
					+ e.getMessage());

		} catch (IllegalAccessException e) {
			throw new SQLException("Cannot create " + c.getName() + ": "
					+ e.getMessage());
		}
	}

	private Object createBean(ResultSet rs, Class type,
			PropertyDescriptor[] props, int[] columnToProperty)
			throws SQLException {
		Object bean = this.newInstance(type);

		ResultSetMetaData rsmd = rs.getMetaData();
		int columnCount = rsmd.getColumnCount();
		int[] columnTypes = new int[columnCount + 1];// 字段类型
		String[] columnNames = new String[columnCount + 1];// 字段名
		columnTypes[0] = -1;
		for (int i = 1; i <= columnCount; i++) {
			columnTypes[i] = rsmd.getColumnType(i);
			columnNames[i] = rsmd.getColumnName(i);
		}

		for (int i = 1; i < columnToProperty.length; i++) {

			if (columnToProperty[i] == PROPERTY_NOT_FOUND) {
				continue;
			}
			//log.debug("columnNames="+columnNames[i]);
			PropertyDescriptor prop = props[columnToProperty[i]];
			Class propType = prop.getPropertyType();

			// 以前的方式
			Object value = this.processColumn(rs, i, propType, columnTypes[i]);
			
			//log.debug("value="+value+",value is not null");

			if (propType != null && value == null && propType.isPrimitive()) {
				value = primitiveDefaults.get(propType);
			}
			
			//log.debug("value="+value+",value is not null");
			
			this.callSetter(bean, prop, value);
		}
		return bean;
	}

	/**
	 * Calls the setter method on the target object for the given property. If
	 * no setter method exists for the property, this method does nothing.
	 * 
	 * @param target
	 *            The object to set the property on.
	 * @param prop
	 *            The property to set.
	 * @param value
	 *            The value to pass into the setter.
	 * @throws SQLException
	 *             if an error occurs setting the property.
	 */
	private void callSetter(Object target, PropertyDescriptor prop, Object value)
			throws SQLException {

		Method setter = prop.getWriteMethod();

		if (setter == null) {
			return;
		}
		Class[] params = setter.getParameterTypes();
		try {
			// Don't call setter if the value object isn't the right type
			if (this.isCompatibleType(value, params[0])) {
				setter.invoke(target, new Object[] { value });
			} else {
				log.debug("the value object isn't the right type");
			}

		} catch (IllegalArgumentException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": "
					+ e.getMessage());

		} catch (IllegalAccessException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": "
					+ e.getMessage());

		} catch (InvocationTargetException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": "
					+ e.getMessage());
		}
	}

	/**
	 * ResultSet.getObject() returns an Integer object for an INT column. The
	 * setter method for the property might take an Integer or a primitive int.
	 * This method returns true if the value can be successfully passed into the
	 * setter method. Remember, Method.invoke() handles the unwrapping of
	 * Integer into an int.
	 * 
	 * @param value
	 *            The value to be passed into the setter method.
	 * @param type
	 *            The setter's parameter type.
	 * @return boolean True if the value is compatible.
	 */
	private boolean isCompatibleType(Object value, Class type) {
		if (value == null || type.isInstance(value)) {
			return true;

		} else if (type.equals(Integer.TYPE) && Integer.class.isInstance(value)) {
			return true;

		} else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) {
			return true;

		} else if (type.equals(Double.TYPE) && Double.class.isInstance(value)) {
			return true;

		} else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) {
			return true;

		} else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) {
			return true;

		} else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) {
			return true;

		} else if (type.equals(Character.TYPE)
				&& Character.class.isInstance(value)) {
			return true;

		} else if (type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) {
			return true;

			// } else if (type.equals(String.class)) {//byb add 20060919
			// return true;

		} else {
			return false;
		}

	}

	private Object processColumn(ResultSet rs, int index, Class propType,
			int columnType) throws SQLException {
		return fieldMapper.mapRow(rs, index, propType, columnType);
	}

	/**
	 * Returns a PropertyDescriptor[] for the given Class.
	 * 
	 * @param c
	 *            The Class to retrieve PropertyDescriptors for.
	 * @return A PropertyDescriptor[] describing the Class.
	 * @throws SQLException
	 *             if introspection failed.
	 */
	private PropertyDescriptor[] propertyDescriptors(Class c)
			throws SQLException {
		// Introspector caches BeanInfo classes for better performance
		BeanInfo beanInfo = null;
		try {
			beanInfo = Introspector.getBeanInfo(c);

		} catch (IntrospectionException e) {
			throw new SQLException("Bean introspection failed: "
					+ e.getMessage());
		}

		return beanInfo.getPropertyDescriptors();
	}

	/**
	 * Convert a <code>ResultSet</code> row into a JavaBean. This
	 * implementation uses reflection and <code>BeanInfo</code> classes to
	 * match column names to bean property names. Properties are matched to
	 * columns based on several factors: <br/>
	 * <ol>
	 * <li>The class has a writable property with the same name as a column.
	 * The name comparison is case insensitive.</li>
	 * 
	 * <li>The column type can be converted to the property's set method
	 * parameter type with a ResultSet.get* method. If the conversion fails (ie.
	 * the property was an int and the column was a Timestamp) an SQLException
	 * is thrown.</li>
	 * </ol>
	 * 
	 * <p>
	 * Primitive bean properties are set to their defaults when SQL NULL is
	 * returned from the <code>ResultSet</code>. Numeric fields are set to 0
	 * and booleans are set to false. Object bean properties are set to
	 * <code>null</code> when SQL NULL is returned. This is the same behavior
	 * as the <code>ResultSet</code> get* methods.
	 * </p>
	 */
	public Object toBean(ResultSet rs, Class type) throws SQLException {
		PropertyDescriptor[] props = this.propertyDescriptors(type);

		ResultSetMetaData rsmd = rs.getMetaData();
		int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

		return this.createBean(rs, type, props, columnToProperty);
	}

	/**
	 * Convert a <code>ResultSet</code> into a <code>List</code> of
	 * JavaBeans. This implementation uses reflection and <code>BeanInfo</code>
	 * classes to match column names to bean property names. Properties are
	 * matched to columns based on several factors: <br/>
	 * <ol>
	 * <li>The class has a writable property with the same name as a column.
	 * The name comparison is case insensitive.</li>
	 * 
	 * <li>The column type can be converted to the property's set method
	 * parameter type with a ResultSet.get* method. If the conversion fails (ie.
	 * the property was an int and the column was a Timestamp) an SQLException
	 * is thrown.</li>
	 * </ol>
	 * 
	 * <p>
	 * Primitive bean properties are set to their defaults when SQL NULL is
	 * returned from the <code>ResultSet</code>. Numeric fields are set to 0
	 * and booleans are set to false. Object bean properties are set to
	 * <code>null</code> when SQL NULL is returned. This is the same behavior
	 * as the <code>ResultSet</code> get* methods.
	 * </p>
	 */
	public List toBeanList(ResultSet rs, Class type) throws SQLException {
		List results = new ArrayList();

		if (!rs.next()) {
			return results;
		}

		PropertyDescriptor[] props = this.propertyDescriptors(type);
		ResultSetMetaData rsmd = rs.getMetaData();
		int[] columnToProperty = this.mapColumnsToProperties(rsmd, props);

		do {
			results.add(this.createBean(rs, type, props, columnToProperty));
		} while (rs.next());

		return results;
	}

	/**
	 * The positions in the returned array represent column numbers. The values
	 * stored at each position represent the index in the
	 * <code>PropertyDescriptor[]</code> for the bean property that matches
	 * the column name. If no bean property was found for a column, the position
	 * is set to <code>PROPERTY_NOT_FOUND</code>.
	 * 
	 * @param rsmd
	 *            The <code>ResultSetMetaData</code> containing column
	 *            information.
	 * 
	 * @param props
	 *            The bean property descriptors.
	 * 
	 * @return An int[] with column index to property index mappings. The 0th
	 *         element is meaningless because JDBC column indexing starts at 1.
	 *         该数组保存数据库查询结果的col和bean属性数组的对应关系
	 * @throws SQLException
	 */
	protected int[] mapColumnsToProperties(ResultSetMetaData rsmd,
			PropertyDescriptor[] props) throws SQLException {

		int cols = rsmd.getColumnCount();
		int columnToProperty[] = new int[cols + 1];
		Arrays.fill(columnToProperty, PROPERTY_NOT_FOUND);

		for (int col = 1; col <= cols; col++) {
			String columnName = rsmd.getColumnName(col);
			for (int i = 0; i < props.length; i++) {

				if (columnName.equalsIgnoreCase(props[i].getName())) {
					columnToProperty[col] = i;
					break;
				}
			}
		}

		return columnToProperty;
	}

	private FieldMapper fieldMapper;

	public FieldMapper getFieldMapper() {
		return fieldMapper;
	}

	public void setFieldMapper(FieldMapper fieldMapper) {
		this.fieldMapper = fieldMapper;
	}

}